package com.bstek.uflo.designer.graph.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import com.bstek.uflo.designer.graph.GraphService;
import com.bstek.uflo.designer.model.Point;
import com.bstek.uflo.designer.model.Rectangle;
import com.bstek.uflo.designer.model.Shape;
import com.bstek.uflo.designer.model.edge.Connection;
import com.bstek.uflo.designer.model.node.Node;
import com.bstek.uflo.designer.model.process.Process;
import com.bstek.uflo.designer.security.manager.SecurityManager;
import com.bstek.uflo.designer.security.model.NodeEntry;
import com.bstek.uflo.designer.security.model.NodeSecurityAttribute;
import com.bstek.uflo.designer.security.model.ProcessAttribute;
import com.bstek.uflo.designer.security.model.ProcessEntry;
import com.bstek.uflo.designer.utils.EscapeUtils;
import com.bstek.uflo.designer.utils.JSONUtils;
import com.bstek.uflo.designer.utils.ShapeUtils;

/**
 * @author matt.yao@bstek.com
 * @since 1.0
 */
@Service(GraphService.BEAN_ID)
public class GraphServiceImpl implements GraphService {

	@Autowired
	@Qualifier(SecurityManager.BEAN_ID)
	private SecurityManager securityManager;

	public Process serializer(Document doc) throws Exception {
		Element root = doc.getRootElement();
		String entityData = root.attributeValue(ENTITY_DATA);
		Process process = StringUtils.hasText(entityData) ? JSONUtils.fromJSON(EscapeUtils.unescape(entityData), Process.class) : new Process("process");
		process.cleanUselessValue();
		List<Connection> connections = new ArrayList<Connection>();
		for (Object elementObj : root.elements()) {
			Element element = (Element) elementObj;
			String clazz = element.attributeValue("clazz");
			String nodeEntryId = element.attributeValue("nodeEntryId");
			entityData = element.attributeValue(ENTITY_DATA);
			Object obj = StringUtils.hasText(entityData) ? JSONUtils.fromJSON(EscapeUtils.unescape(entityData), Class.forName(clazz)) : Class.forName(clazz).newInstance();
			if (obj instanceof Shape) {
				this.fillCommonAttributeValue((Shape) obj, element);
			}
			if (obj instanceof Connection) {
				Connection connection = (Connection) obj;
				connection.cleanUselessValue();
				connection.setName(EscapeUtils.unescape(element.attributeValue("toLabel")));
				String from = element.attributeValue("from");
				String to = element.attributeValue("to");
				if (StringUtils.hasText(from) && StringUtils.hasText(to)) {
					String fromNodeId = from.substring(0, element.attributeValue("from").indexOf("_TERMINAL"));
					String toNodeId = to.substring(0, element.attributeValue("to").indexOf("_TERMINAL"));
					String vertices = EscapeUtils.unescape(element.attributeValue("vertices"));
					connection.setFromNodeId(fromNodeId);
					connection.setToNodeId(toNodeId);
					connection.setVertices(this.buildConnectionPoints(vertices));
					connection.setNodeEntryId(nodeEntryId);
					connections.add(connection);
				}
			} else if (obj instanceof Node) {
				Node node = (Node) obj;
				node.cleanUselessValue();
				node.setName(EscapeUtils.unescape(element.attributeValue("label")));
				process.getNodes().add(node);
				if (StringUtils.hasText(nodeEntryId)) {
					securityManager.updateNodeEntry(nodeEntryId, node.getName());
				}

			}
		}
		for (Connection connection : connections) {
			String fromNodeId = connection.getFromNodeId();
			String toNodeId = connection.getToNodeId();
			Node fromNode = ShapeUtils.findNodeById(process.getNodes(), fromNodeId);
			Node toNode = ShapeUtils.findNodeById(process.getNodes(), toNodeId);
			if (fromNode != null && toNode != null) {
				String nodeEntryId = connection.getNodeEntryId();
				if (StringUtils.hasText(nodeEntryId)) {
					String name = connection.getName();
					if (!StringUtils.hasText(name)) {
						name = "to " + toNode.getName();
					}
					securityManager.updateNodeEntry(nodeEntryId, name);
				}
				connection.setFromNode(fromNode);
				connection.setToNode(toNode);
				fromNode.getOutConnections().add(connection);
			}
		}
		return process;
	}

	public Document deserializer(Process process) throws Exception {
		List<Connection> connections = process.getConnections();
		List<Node> nodes = process.getNodes();
		List<Map<String, String>> cellAttributeList = new ArrayList<Map<String, String>>();
		Map<String, String> cellAttribute = null;
		for (Connection connection : connections) {
			String fromNodeId = connection.getFromNodeId();
			String toNodeName = connection.getToNodeName();
			Node fromNode = ShapeUtils.findNodeById(process.getNodes(), fromNodeId);
			Node toNode = ShapeUtils.findNodeByName(process.getNodes(), toNodeName);
			String toNodeId = toNode.getId();
			connection.setToNodeId(toNodeId);
			if (StringUtils.hasText(fromNode.getToNodeId())) {
				fromNode.setToNodeId(fromNode.getToNodeId() + "," + toNodeId);
			} else {
				fromNode.setToNodeId(toNodeId);
			}
			if (StringUtils.hasText(fromNode.getToConnectionId())) {
				fromNode.setToConnectionId(fromNode.getToConnectionId() + "," + connection.getId());
			} else {
				fromNode.setToConnectionId(connection.getId());
			}
			if (StringUtils.hasText(toNode.getFromNodeId())) {
				toNode.setFromNodeId(toNode.getFromNodeId() + "," + fromNodeId);
			} else {
				toNode.setFromNodeId(fromNodeId);
			}
			if (StringUtils.hasText(toNode.getFromConnectionId())) {
				toNode.setFromConnectionId(toNode.getFromConnectionId() + "," + connection.getId());
			} else {
				toNode.setFromConnectionId(connection.getId());
			}
			cellAttribute = new HashMap<String, String>();
			cellAttribute.put("id", connection.getId());
			cellAttribute.put("shapeType", connection.getShapeType());
			cellAttribute.put("shapeId", connection.getShapeId());
			cellAttribute.put("style", EscapeUtils.escape(connection.getEdgeStyle()));
			cellAttribute.put("toLabel", EscapeUtils.escape(connection.getName()));
			cellAttribute.put("value", connection.getEdgeValue(fromNode, toNode));
			cellAttribute.put("geom", EscapeUtils.escape(connection.getEdgeGeomValue(fromNode, toNode)));
			cellAttribute.put("from", connection.getEdgeFromValue());
			cellAttribute.put("to", connection.getEdgeToValue());
			cellAttribute.put(ENTITY_DATA, EscapeUtils.escape(connection.toJSON()));
			this.addSecurityAttribute(process.getProcessEntry(), connection, cellAttribute);
			cellAttributeList.add(cellAttribute);
		}
		for (Node node : nodes) {
			cellAttribute = new HashMap<String, String>();
			int x = node.getRectangle().getPoint().getX();
			int y = node.getRectangle().getPoint().getY();
			int width = node.getRectangle().getWidth();
			int height = node.getRectangle().getHeight();
			cellAttribute.put("id", node.getId());
			cellAttribute.put("x", String.valueOf(x));
			cellAttribute.put("y", String.valueOf(y));
			cellAttribute.put("width", String.valueOf(width));
			cellAttribute.put("height", String.valueOf(height));
			cellAttribute.put("label", node.getName());
			cellAttribute.put("shapeId", node.getShapeId());
			cellAttribute.put("shapeType", node.getShapeType());
			cellAttribute.put("value", node.getShapeValue());
			cellAttribute.put("geom", node.getShapeGeomValue());
			cellAttribute.put(ENTITY_DATA, EscapeUtils.escape(node.toJSON()));
			String toNodeId = node.getToNodeId();
			if (StringUtils.hasText(toNodeId)) {
				cellAttribute.put("to", toNodeId);
				cellAttribute.put("toEdge", node.getToConnectionId());
			}
			String fromNodeId = node.getFromNodeId();
			if (StringUtils.hasText(fromNodeId)) {
				cellAttribute.put("from", fromNodeId);
				cellAttribute.put("fromEdge", node.getFromConnectionId());
			}
			this.addSecurityAttribute(process.getProcessEntry(), node, cellAttribute);
			cellAttributeList.add(cellAttribute);
		}
		Document document = this.buildDocument(process, cellAttributeList);
		return document;
	}

	private void addSecurityAttribute(ProcessEntry processEntry, Shape shape, Map<String, String> cellAttribute) throws Exception {
		if (processEntry != null) {
			List<NodeEntry> nodeEntryList = processEntry.getNodeEntrylist();
			if (nodeEntryList != null) {
				String name = null;
				if (shape instanceof Connection) {
					Connection con = (Connection) shape;
					name = StringUtils.hasText(con.getName()) ? con.getName() : "to " + con.getToNodeName();
				} else if (shape instanceof Node) {
					name = ((Node) shape).getName();
				}
				if (StringUtils.hasText(name)) {
					NodeEntry nodeEntry = this.findNodeEntry(name, nodeEntryList);
					if (nodeEntry != null && nodeEntry.isPersistence()) {
						cellAttribute.put("nodeEntryId", nodeEntry.getId());
						String nodeEntryData = JSONUtils.toJSON(nodeEntry);
						cellAttribute.put("nodeEntryData", EscapeUtils.escape(nodeEntryData));
					}
				}
			}
		}
	}

	private NodeEntry findNodeEntry(String name, List<NodeEntry> nodeEntryList) {
		for (NodeEntry nodeEntry : nodeEntryList) {
			if (nodeEntry.getName().equals(name)) {
				return nodeEntry;
			}
		}
		return null;
	}

	private Document buildDocument(Process process, List<Map<String, String>> cellAttributeList) throws Exception {
		ProcessEntry processEntry = process.getProcessEntry();
		String rootEntityJsonData = EscapeUtils.escape(process.toJSON());
		Document document = DocumentHelper.createDocument();
		Element opengraphElement = document.addElement(ROOT_ELEMENT_NAME);
		opengraphElement.addAttribute("width", GRAPH_WIDTH);
		opengraphElement.addAttribute("height", GRAPH_HEIGHT);
		opengraphElement.addAttribute(ENTITY_DATA, rootEntityJsonData);
		if (processEntry != null) {
			List<ProcessAttribute> processAttributeList = processEntry.getProcessAttributeList();
			if (processAttributeList != null) {
				String processAttributes = JSONUtils.toJSON(processAttributeList);
				opengraphElement.addAttribute("processAttributes", EscapeUtils.escape(processAttributes));
			}
			List<NodeSecurityAttribute> nodeSecurityAttributeList = processEntry.getNodeSecurityAttributeList();
			if (nodeSecurityAttributeList != null) {
				String nodeSecurityAttributes = JSONUtils.toJSON(nodeSecurityAttributeList);
				opengraphElement.addAttribute("nodeSecurityAttributes", EscapeUtils.escape(nodeSecurityAttributes));
			}
		}
		for (Map<String, String> cell : cellAttributeList) {
			Element cellElement = opengraphElement.addElement(ELEMENT_NAME);
			for (Map.Entry<String, String> entry : cell.entrySet()) {
				cellElement.addAttribute(entry.getKey(), entry.getValue());
			}
		}
		return document;
	}

	private void fillCommonAttributeValue(Shape shape, Element element) {
		shape.setId(element.attributeValue("id"));
		int x = Integer.parseInt(element.attributeValue("x"));
		int y = Integer.parseInt(element.attributeValue("y"));
		int width = Integer.parseInt(element.attributeValue("width"));
		int height = Integer.parseInt(element.attributeValue("height"));
		Point point = new Point(x, y);
		shape.setRectangle(new Rectangle(point, width, height));
	}

	private List<Point> buildConnectionPoints(String vertices) throws Exception {
		List<Point> points = new ArrayList<Point>();
		Pattern pattern = Pattern.compile(".(\\d+,\\d+).");
		Matcher matcher = pattern.matcher(vertices);
		while (matcher.find()) {
			Integer[] values = JSONUtils.fromJSON(matcher.group(), Integer[].class);
			points.add(new Point(values[0], values[1]));
		}
		return points;
	}

}
